﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using ValidationEngine.Commons.Enums;
using DevExpress.XtraEditors.DXErrorProvider;
using ValidationEngine.Commons.Interfaces;

namespace ValidationEngine.Commons
{
    /// <summary>
    /// Stores model validation results. Provides functionality to manage model errors.
    /// </summary>
    internal class ModelValidationProperties
    {
        #region Members
        /// <summary>
        /// Stores cached erros
        /// </summary>
        private Hashtable _errorsCache;
        /// <summary>
        /// Stores cached warnings
        /// </summary>
        private Hashtable _warningsCache;
        /// <summary>
        /// Stores cached infos
        /// </summary>
        private Hashtable _infosCache;

        /// <summary>
        /// TModel properties collection
        /// </summary>
        private readonly List<PropertyInfo> _properties = new List<PropertyInfo>();
        /// <summary>
        /// TModel properties of ValidateableModel type collection
        /// </summary>
        private readonly List<PropertyInfo> _validableMembers = new List<PropertyInfo>();
        /// <summary>
        /// TModel instance
        /// </summary>
        private IValidateableModel _model;

        /// <summary>
        /// Event to call validation handler in subscribed ValidationProvider
        /// </summary>
        public event PropertyValidationEventHandler PropertyValidating; 
        #endregion

        #region Ctor
        public ModelValidationProperties(IValidateableModel model)
        {
            this._model = model;
            SubscribeToPropertiesChanges();
        } 
        #endregion

        #region Properties
        internal Hashtable Errors
        {
            get { return _errorsCache; }
        }

        internal Hashtable Warnings
        {
            get { return _warningsCache; }
        }

        internal Hashtable Infos
        {
            get { return _infosCache; }
        }

        internal bool IsValid
        {
            get 
            {
                bool valid = _errorsCache.Values.Cast<string>().Where(v => !v.IsNullOrEmpty()).Count() == 0;
                if (!valid)
                {
                    return valid;
                }
                var modelType = _model.GetType();
                foreach (var property in _validableMembers)
                {
                    var propVal = property.GetValue(_model, null);
                    if (null != propVal)
                    {
                        if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(BindingList<>))
                        {
                            foreach (var item in (IEnumerable)propVal)
                            {
                                valid &= ((IValidateableModel)item).IsValid;
                                if (!valid)
                                {
                                    break;
                                }
                            }
                        }
                        else
                        {
                            valid &= ((IValidateableModel)propVal).IsValid;
                        }
                    }
                    if (!valid)
                    {
                        break;
                    }
                }
                return valid;
            }
        }
        #endregion

        #region Errors Cache Tools

        /// <summary>
        /// Retrives property error from cache
        /// </summary>
        /// <param name="propName">Property name</param>
        /// <returns>Error text or NULL if no</returns>
        internal string GetErrorFromCache(string propName)
        {
            return _errorsCache.ContainsKey(propName) ? (string)_errorsCache[propName] : null;
        }

        /// <summary>
        /// Sets error for property 
        /// </summary>
        /// <param name="propName">Property name</param>
        /// <param name="msg">Error text</param>
        internal void SetErrorForProperty(string propName, string msg)
        {
            _errorsCache[propName] = msg;
        }
        #endregion

        #region Warnings Cache Tools

        /// <summary>
        /// Retrives property warning from cache
        /// </summary>
        /// <param name="propName">Property name</param>
        /// <returns>Warning text or NULL if no</returns>
        internal string GetWarningFromCache(string propName)
        {
            return _warningsCache.ContainsKey(propName) ? (string)_warningsCache[propName] : null;
        }

        /// <summary>
        /// Sets warning for property
        /// </summary>
        /// <param name="propName">Property name</param>
        /// <param name="msg">Warning text</param>
        internal void SetWarningForProperty(string propName, string msg)
        {
            _warningsCache[propName] = msg;
        }
        #endregion

        #region Infos Cache Tools

        /// <summary>
        /// Retrives property info from cache
        /// </summary>
        /// <param name="propName">Property name</param>
        /// <returns>Info text or NULL if no</returns>
        internal string GetInfoFromCache(string propName)
        {
            return _infosCache.ContainsKey(propName) ? (string)_infosCache[propName] : null;
        }

        /// <summary>
        /// Sets info for property
        /// </summary>
        /// <param name="propName">Property name</param>
        /// <param name="msg">Info text</param>
        internal void SetInfoForProperty(string propName, string msg)
        {
            _infosCache[propName] = msg;
        }
        #endregion

        #region Tools
        /// <summary>
        /// Returns error type for property according to errors priority
        /// </summary>
        /// <param name="propertyName">Property name</param>
        /// <returns>Error type</returns>
        private ValidationErrorType GetErrorType(string propertyName)
        {
            if (null != _errorsCache[propertyName])
            {
                return ValidationErrorType.Error;
            }
            else if (null != _warningsCache[propertyName])
            {
                return ValidationErrorType.Warning;
            }
            else if (null != _infosCache[propertyName])
            {
                return ValidationErrorType.Information;
            }
            return ValidationErrorType.None;
        }

        /// <summary>
        /// Scan model's properties and subscribes to INotifyValueChanged. Scans throw out full object hierarhy deep.
        /// </summary>
        private void SubscribeToPropertiesChanges()
        {
            Type modelType = _model.GetType();
            var properties = modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
            _errorsCache = new Hashtable(properties.Length);
            _warningsCache = new Hashtable(properties.Length);
            _infosCache = new Hashtable(properties.Length);

            foreach (PropertyInfo property in properties)
            {
                if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(BindingList<>))
                {
                    var propVal = property.GetValue(_model, null);
                    Type itemsType = property.PropertyType.GetGenericArguments()[0];
                    registerProperty(property, itemsType);
                }
                else
                {
                    registerProperty(property, property.PropertyType);
                }
            }
        }

        /// <summary>
        /// Adds property to properties cache and inits validation caches
        /// </summary>
        /// <param name="pi">Property info</param>
        /// <param name="type">Property member type</param>
        private void registerProperty(PropertyInfo pi, Type type)
        {
            _errorsCache.Add(pi.Name, null);
            _warningsCache.Add(pi.Name, null);
            _infosCache.Add(pi.Name, null);

            if (typeof(IValidateableModel).IsAssignableFrom(type))
            {
                _validableMembers.Add(pi);
            }
            else
            {
                _properties.Add(pi);
            }
        } 
        #endregion

        #region Internal methods
        /// <summary>
        /// Raises PropertyValidating event for property
        /// </summary>
        /// <param name="propName">Property name</param>
        /// <param name="ctx">Rule appling context</param>
        internal void RaiseValidateProperty(string propName, RuleApplingContext ctx = RuleApplingContext.Always)
        {
            if (null != PropertyValidating)
            {
                var args = new PropertyValidationEventArgs
                { 
                    Context = new ValidationContext 
                    { 
                        Model = _model, 
                        PropertyName = propName, 
                        ApplingContext = ctx
                    }
                };
                PropertyValidating(args);
                SetPropertyValidationInfo(propName, args.ValidationInfo);
            }
        }

        /// <summary>
        /// Updates validation cache for property
        /// </summary>
        /// <param name="propName"></param>
        /// <param name="vi"></param>
        internal void SetPropertyValidationInfo(string propName, IValidationInfo vi)
        {
            _errorsCache[propName] = vi.ErrorText;
            _warningsCache[propName] = vi.WarningText;
            _infosCache[propName] = vi.InfoText;
        }

        /// <summary>
        /// Returns property validation according to error priority
        /// </summary>
        /// <param name="propertyName">Property name</param>
        /// <returns>Validation string</returns>
        internal string GetCachedPropertyValidation(string propertyName)
        {
            return (string)(_errorsCache[propertyName] ?? _warningsCache[propertyName] ?? _infosCache[propertyName]);
        }

        /// <summary>
        /// Clears model validation cache
        /// </summary>
        internal void ClearValidationsInfo()
        {
            _errorsCache.Clear();
            _warningsCache.Clear();
            _infosCache.Clear();
        }

        #region IDXErrorDataInfo
        internal void GetError(ErrorInfo info) // parasoft-suppress PB.EMPTYMETHODS this method is part of interface and can not be avoided.
        {
        }

        internal void GetPropertyError(string propertyName, ErrorInfo info)
        {
            info.ErrorText = (string)(_errorsCache[propertyName] ?? _warningsCache[propertyName] ?? _infosCache[propertyName]);
            info.ErrorType = (info.ErrorText == null ? ValidationErrorType.None : GetErrorType(propertyName)).GetDXErrType();
        } 
        #endregion


        /// <summary>
        /// Returns validation info for property
        /// </summary>
        /// <param name="propertyName">Property name</param>
        /// <returns>Validation info</returns>
        internal IValidationInfo GetPropertyValidationInfo(string propertyName)
        {
            var vi = new ValidationInfo();
            vi.AddError((string)_errorsCache[propertyName]);
            vi.AddWarning((string)_warningsCache[propertyName]);
            vi.AddInfo((string)_infosCache[propertyName]);
            return vi;
        }


        /// <summary>
        /// Clears validation cache for property
        /// </summary>
        /// <param name="propertyName">Property name</param>
        internal void ClearPropertyValidationInfo(string propertyName)
        {
            _errorsCache[propertyName] = null;
            _warningsCache[propertyName] = null;
            _infosCache[propertyName] = null;
        }

        /// <summary>
        /// Revalidate model with rule appling context
        /// </summary>
        /// <param name="ctx">Appling context</param>
        internal void ReValidate(RuleApplingContext ctx)
        {
            foreach (var property in _properties)
            {
                RaiseValidateProperty(property.Name, ctx);
            }
            foreach (var property in _validableMembers)
            {
                Type modelType = _model.GetType();
                var propVal = property.GetValue(_model, null);
                if (null != propVal)
                {
                    if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(BindingList<>))
                    {
                        foreach (var item in (IEnumerable)propVal)
                        {
                            ((IValidateableModel)item).ReValidate(ctx);
                        }
                    }
                    else
                    {
                        ((IValidateableModel)propVal).ReValidate(ctx);
                    }
                }
                RaiseValidateProperty(property.Name, ctx);
            }
        } 
        #endregion
    }
}
